Python3 爬虫教程:Ajax 数据爬取实战
开篇小提示:如果你练过爬虫,大概率遇到过「直接用 requests 爬页面,HTML里全是骨架没有数据」的情况——这几乎就是Ajax动态加载的典型信号。这篇我们用 崔庆才老师的免费靶站 spa1 走一遍完整流程,接口规则清晰,没有复杂反爬,很适合入门巩固。
1. 准备工作
在开始前,请确保你的环境满足以下要求:
- 已安装 Python 3.6+
- 已通过
pip install requests pymongo 安装依赖库
- 本地已启动 MongoDB 服务(用于最后数据存储,没有的话可以只跑到爬取打印环节)
- 对浏览器开发者工具(F12)的 Network 面板有基本了解
2. 目标网站分析
我们的靶站地址是:https://spa1.scrape.center/
2.1 网站核心特点
✅ 数据完全通过 Ajax 异步加载
✅ 页面内容由前端 JS 渲染,HTML 仅含基础布局
✅ 支持分页浏览(共10页左右,每页10部电影)
✅ 点击卡片可跳转至详情页,详情页数据也是 Ajax 接口
2.2 需要爬取的字段
从列表页和详情页我们可以提取完整的电影信息:
- 电影名称
- 封面图片链接
- 类别标签(如「剧情」「悬疑」)
- 上映日期
- 综合评分
- 剧情简介
3. 初步验证:静态HTML抓不到数据
先写个最简单的 requests 测试,看看直接爬页面会拿到什么:
import requests
url = 'https://spa1.scrape.center/'
response = requests.get(url)
print(response.text[:500]) # 只打印前500字,避免太长
运行后你会发现,HTML 里只有 <div id="app"></div> 这种容器标签,连「电影」「评分」这些关键词都找不到——完全符合我们的预期!接下来就得靠开发者工具找Ajax接口了。
4. 爬取列表页
4.1 分析列表页的Ajax接口
打开浏览器按 F12,切换到 Network 面板,记得勾选 Preserve Log(翻页后不会清空请求记录),再筛选 XHR/Fetch(只看Ajax类请求)。
现在点击页面的「第2页」「第3页」,观察 Network 里新增的请求,会发现它们的 URL 格式高度统一:
https://spa1.scrape.center/api/movie/?limit=10&offset=0
https://spa1.scrape.center/api/movie/?limit=10&offset=10
https://spa1.scrape.center/api/movie/?limit=10&offset=20
参数解析
limit: 固定为10,指每页返回的电影数量
offset: 数据偏移量,第1页是0,第2页是10,第n页就是 10*(n-1)
4.2 封装列表页爬取函数
为了代码复用性,我们先写一个通用的接口爬取函数,再专门写列表页的调用函数:
import requests
import logging
# 配置日志,方便追踪爬取进度和错误
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s: %(message)s'
)
# 全局常量
INDEX_URL = 'https://spa1.scrape.center/api/movie/?limit={limit}&offset={offset}'
LIMIT = 10 # 固定每页10条数据
def scrape_api(url):
"""通用的Ajax接口爬取函数:返回JSON数据或None"""
logging.info('正在爬取接口:%s', url)
try:
response = requests.get(url)
if response.status_code == 200:
return response.json() # 直接返回解析后的JSON字典
logging.error('爬取失败:接口 %s 返回状态码 %d', url, response.status_code)
except requests.RequestException:
logging.error('爬取出错:接口 %s 发生请求异常', url, exc_info=True)
return None
def scrape_index(page):
"""爬取指定页的电影列表"""
url = INDEX_URL.format(limit=LIMIT, offset=LIMIT * (page - 1))
return scrape_api(url)
5. 爬取详情页
5.1 分析详情页的Ajax接口
在列表页随便点击一部电影的卡片,进入详情页后同样筛选 XHR/Fetch,会发现一条新的类似请求:
https://spa1.scrape.center/api/movie/1/
很明显,这里的 1 是电影的唯一ID——这个ID在列表页的JSON数据里已经有了(字段名就是 id),所以我们不用解析HTML跳转链接,直接拿列表页的ID去拼接接口即可。
5.2 封装详情页爬取函数
复用刚才的 scrape_api 函数,很快就能写完:
DETAIL_URL = 'https://spa1.scrape.center/api/movie/{id}/'
def scrape_detail(movie_id):
"""爬取指定ID的电影详情"""
url = DETAIL_URL.format(id=movie_id)
return scrape_api(url)
6. 数据存储:存入MongoDB
靶站的接口返回了完整的JSON数据,我们可以直接用 MongoDB 这种文档型数据库存储,不用额外处理字段映射(非常方便!)。
6.1 配置MongoDB连接
先导入 pymongo 并连接本地数据库:
import pymongo
# MongoDB全局常量
MONGO_URI = 'mongodb://localhost:27017'
MONGO_DB = 'movies_spa1'
MONGO_COLLECTION = 'movies'
# 建立连接并获取集合
client = pymongo.MongoClient(MONGO_URI)
db = client[MONGO_DB]
collection = db[MONGO_COLLECTION]
6.2 封装数据保存函数
用 update_one 配合 upsert=True,可以实现数据去重更新:如果数据库里已经有同名电影,就更新它的信息;如果没有,就新增一条。
def save_data(data):
"""将电影数据存入MongoDB,同名电影去重更新"""
if not data:
return
collection.update_one(
{'name': data.get('name')}, # 用电影名作为去重条件
{'$set': data}, # 只更新/设置传入的字段
upsert=True # 没有匹配的记录则新增
)
logging.info('电影《%s》已保存成功!', data.get('name'))
7. 完整爬取流程
最后把所有函数串起来,写一个主函数,爬取前10页的所有电影:
def main():
for page in range(1, 11): # 爬取第1到第10页
index_data = scrape_index(page)
if not index_data or not index_data.get('results'):
continue
# 遍历当前页的每部电影,获取ID后爬详情、存库
for item in index_data['results']:
movie_id = item.get('id')
detail_data = scrape_detail(movie_id)
save_data(detail_data)
if __name__ == '__main__':
main()
8. 现代爬虫技术改进建议(简单版)
这篇是入门实战,代码比较基础,如果你想提升效率和稳定性,可以试试这些小优化:
- 异步爬取:用
aiohttp 替代 requests,多任务并发,速度会快很多
- 错误重试:给
scrape_api 加 tenacity 库的重试装饰器,解决网络波动问题
- 反爬基础:添加随机
User-Agent 和 time.sleep() 模拟人类操作
- 数据校验:保存前检查必填字段(如
name id)是否存在,避免脏数据
9. 总结
通过这篇实战,我们掌握了 Ajax动态加载数据的爬虫完整流程:
- 先用静态HTML测试,验证数据是否动态加载
- 用浏览器开发者工具找列表页和详情页的Ajax接口
- 分析接口参数,封装通用爬取函数
- 串起流程、加日志、存数据库
完整代码可以直接复制运行,别忘了先启动MongoDB哦!